iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 20
1
Mobile Development

程式初學:Android與Kotlin系列 第 20

Day 20--天氣app(六)類型檢測與類型轉換,在fragment建立更新的方法

  • 分享至 

  • xImage
  •  

第一種做法

因爲在viewpager2的adapter有fragment的實例了
所以要做的就是當在MainActivity送出搜尋時
各fragment要有方法可以update

類型檢測與類型轉換

override fun onCreateOptionsMenu(menu: Menu?): Boolean {
        menuInflater.inflate(R.menu.menu, menu)
        val searchItem = menu?.findItem(R.id.action_search)
        if (searchItem != null) {
            val searchView = searchItem.actionView as SearchView
            searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
                override fun onQueryTextSubmit(query: String?): Boolean {
                    return true
                }

                override fun onQueryTextChange(newText: String?): Boolean {
                    if (newText!!.isNotEmpty()) {
                        weatherList.forEach { location ->
                            if (location.locationName.contains(newText)) {
                                 (viewPager2.adapter as PageAdapter).fragments.forEach {
                                    if (it is FirstFragment) it.updateContent(location)
                                    else if (it is SecondFragment) it.updateContent(location)
                                }
                            }
                        }
                    }
                    return true
                }
            })
        }
        return super.onCreateOptionsMenu(menu)
    }

類型轉換 as

重點在這段,主要表示將PageAdapter的fragments(這是list)全部掃過一遍
然後將搜尋的location內容傳入fragment中的updateContent方法

(viewPager2.adapter as PageAdapter).fragments.forEach {
    if (it is FirstFragment) it.updateContent(location)
    else if (it is SecondFragment) it.updateContent(location)
}

viewPager2.adapter as PageAdapter的意思是
因爲viewPager2.adapter回傳是RecyclerView.Adapter
PageAdapter繼承FragmentStateAdapter

FragmentStateAdapter也繼承自RecyclerView.Adapter

又因爲fragments是PageAdapter的屬性
所以將viewPager2.adapter轉型爲PageAdapter

分解寫的意思就是

但寫成這樣就簡潔有力

這樣viewPager2.adapter就能取得fragments,再對其進行forEach了

類型檢測 is

if (it is FirstFragment) it.updateContent(location)
else if (it is SecondFragment) it.updateContent(location)

在裡面的這二行,it表示forEach讀到的fragment
編譯器也有提示(淺灰色)

if (it is FirstFragment) it.updateContent(location)
若forEach讀到的fragment(第一個it)是FirstFragment,就呼叫FirstFragment(第二個it)的updateContent

else if (it is SecondFragment) it.updateContent(location)
依此類推
若forEach讀到的fragment(第一個it)是SecondFragment,就呼叫SecondFragment(第二個it)的updateContent

參考
https://www.kotlincn.net/docs/reference/typecasts.html

update

再來就是分別在FirstFragment與SecondFragment撰寫updateContent()

FirstFragment

class FirstFragment : Fragment() {
 
 ...
 override fun onCreate(savedInstanceState: Bundle?) {}
 ...

 fun updateContent(location: WeatherData.Records.Location){
        val calendar = Calendar.getInstance()
        val month = calendar.get(Calendar.MONTH)
        val day = calendar.get(Calendar.DAY_OF_MONTH)
        tv_city.text = location.locationName
        tv_currentTime.text =
            (month + 1).toString() + "月" + day.toString() + "日" + SimpleDateFormat(" HH:mm").format(
                System.currentTimeMillis()
            )
        //當日
        tv_startTime1.text = location.weatherElement[0].time[0].startTime
        tv_endTime1.text =
         location.weatherElement[0].time[0].endTime//.split(" ")[1]
        tv_status1.text =
         location.weatherElement[0].time[0].parameter.parameterName
        tv_rainProbability1.text =
         "降雨機率 ${location.weatherElement[1].time[0].parameter.parameterName} %"

        tv_startTime2.text = location.weatherElement[0].time[1].startTime
        tv_endTime2.text =
         location.weatherElement[0].time[1].endTime//.split(" ")[1]
        tv_status2.text =
         location.weatherElement[0].time[1].parameter.parameterName
        tv_rainProbability2.text =
            "降雨機率 ${location.weatherElement[1].time[1].parameter.parameterName} %"
 }
 
}

這樣執行到MainActivity的搜尋if (it is FirstFragment) it.updateContent(location)
就會把搜尋到的location資料作爲引數,並呼叫FirstFragment的updateContent方法
就可把資料給各textview進行畫面更新

但是,又有但是了
還有其它重點須要注意,例如fragment的生命週期

多fragment時,假設搜尋後更新了當前畫面資料(FirstFragment),然後滑動到其它fragment
有可能FirstFragment被Destroy後,再滑回來FirstFragment

FirstFragment重新onCreate以後,畫面就是預設的,並沒有資料

如下圖,搜尋臺南後,FirstFragment有資料了,多滑動幾頁後再回來
FirstFragment被Destroy再重新Create後,就沒有資料了

要用一些方式改善

增設fragment的location,rootView變數

fragment的location變數

因爲當fragment被移除是進入到onDestroyView的lifecycle
先前顯示的資料都會清除
試想..如果fragment被帶回時,會進入onCreatView的lifecycle
那在onCreatView()再次呼叫updateContent方法就可以再顯示資料啦

不過要注意先前搜尋時,是將搜尋取得的WeatherData.Records.Location,作爲引數location再呼叫updateContent方法

而在onCreatView()再次呼叫updateContent方法的話,也是須要再把剛剛搜尋的WeatherData.Records.Location,作爲引數傳入
但現在是在fragment自己的onCreatView()呼叫了,所以須要在開始MainActivity的搜尋時
就要在fragment設置變數記錄這個搜尋的資料,這樣fragment自己呼叫updateContent方法時,就可以拿自己記錄的這個變數傳入
如下:
fragment持有一個fragmentLocation的變數,記錄來自MainActivity的搜尋(location)

fun updateContent(location: WeatherData.Records.Location){
       fragmentLocation = location
       ...
}

fragment就要先宣告fragmentLocation變數
因爲有可能還沒有取得搜尋,所以此變數是可空的
也必須先賦值爲null

class FirstFragment : Fragment() {
        var fragmentLocation: WeatherData.Records.Location? = null
}

再來就是到onCreateView()呼叫updateContent並將fragmentLocation傳入

看到編譯器有提示錯誤(type mismatch)
因爲在fragment宣告fragmentLocation是可空變數
而傳入updateContent的不能是可空變數

最先建立updateContent方法的時候也不會設定讓可空變數傳入
因爲一定是有搜尋資料要更新畫面內容才會呼叫updateContent

按下該行前面的紅色燈泡,可以看到編譯器提示可用?.let{}的方式

正確如下,檢查當fragmentLocation非空時
就執行updateContent(),並將此fragmentLocation(it)傳入

override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
    ...
        fragmentLocation?.let { updateContent(it) } 
    ...
}

run看看

預期應該是搜尋完之後,滑到其它fragment,再滑回來時,FirstFragment再次更新資料並顯示
結果不是像之前一樣沒更新,反而是閃退??

看報錯的log
爲什麼說tv_city must not be null

fragment在onCreateViewinflate的時候已經有回傳inflater.inflate(R.layout.fragment_first, container, false)了

tv_city等的view應該都有建立了,哪裏錯了?

原來當畫面onDestroyView再create後,雖然textView的id還是叫tv_city,但已經不是先前的tv_city
而updateContent可能還要對先前的tv_city更新,但先前的tv_city已經不在了,所以會出錯

fragment的rootView變數

解決方式就是明確的告訴updateContent是對現在所inflate的view更新

設置一個rootView變數來持有現在inflate的layout

class FirstFragment : Fragment() {
    ...
    var rootView: View? = null
    ...
    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        // Inflate the layout for this fragment
        rootView = inflater.inflate(R.layout.fragment_first, container, false)
        
        fragmentLocation?.let { updateContent(it) }   

        return rootView
    }
    ...
}

然後修改updateContent,當rootView非空時,對現在的這個rootView(it)做更新

fun updateContent(location: WeatherData.Records.Location){
        fragmentLocation = location
        rootView?.let {
//            rootView不爲空執行
            val calendar = Calendar.getInstance()
            val month = calendar.get(Calendar.MONTH)
            val day = calendar.get(Calendar.DAY_OF_MONTH)
            it.tv_city.text = location.locationName
            it.tv_currentTime.text =
                (month + 1).toString() + "月" + day.toString() + "日" + SimpleDateFormat(" HH:mm").format(
                    System.currentTimeMillis()
                )
            //當日
            it.tv_startTime1.text = location.weatherElement[0].time[0].startTime
            it.tv_endTime1.text =
                location.weatherElement[0].time[0].endTime//.split(" ")[1]
            it.tv_status1.text =
                location.weatherElement[0].time[0].parameter.parameterName
            it.tv_rainProbability1.text =
                "降雨機率 ${location.weatherElement[1].time[0].parameter.parameterName} %"

            it.tv_startTime2.text = location.weatherElement[0].time[1].startTime
            it.tv_endTime2.text =
                location.weatherElement[0].time[1].endTime//.split(" ")[1]
            it.tv_status2.text =
                location.weatherElement[0].time[1].parameter.parameterName
            it.tv_rainProbability2.text =
                "降雨機率 ${location.weatherElement[1].time[1].parameter.parameterName} %"
        }
 }

最後可以看到頁面滑走再滑回來,不會閃退,資料也會更新回來


上一篇
Day 19--天氣app(五) argument,bundle
下一篇
Day 21--天氣app(七)觀察者模式 Observer Pattern
系列文
程式初學:Android與Kotlin30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言